﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Diagnostics;
using Skugo.Shared;
using System.IO;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Input;
using System.Runtime.InteropServices;

namespace Skugo
{
    /// <summary>
    /// A callback delegate for commands.
    /// </summary>
    public delegate void CommandFn();

    public class CommandSetup
    {
        /// <summary>
        /// Construct a command with a given name.
        /// </summary>
        /// <param name="name">The name of the command, which is required.</param>
        public CommandSetup(String name)
        {
            this.Name = name;
        }

        /// <summary>
        /// The name of the command (a required parameter).
        /// </summary>
        public String Name;

        /// <summary>
        /// The description of the command (used for tool-tips and extra information).
        /// </summary>
        public String Description = String.Empty;

        /// <summary>
        /// The category that the command goes under (typically used for creation of menus).
        /// The category also acts as an extra keyword.
        /// </summary>
        public String Category = String.Empty;

        /// <summary>
        /// An optional icon that we use for the command (displayed in menus, toolbars, etc).
        /// </summary>
        public Icon Icon;

        /// <summary>
        /// The key that we press to invoke the shortcut. To not have a shortcut key, leave it as None.
        /// </summary>
        public Key ShortcutKey;

        /// <summary>
        /// Any keys that must be pressed with the shortcut key (such as control, alt, etc).
        /// To not have a shortcut key modifier, leave it as None.
        /// </summary>
        public KeyModifiers ShortcutModifiers;

        /// <summary>
        /// A list of keywords that pertain to the command (assists the user in searching for items).
        /// </summary>
        public readonly HashSet<String> Keywords = new HashSet<String>();

        /// <summary>
        /// Anyone who wants to listen to the command when it occurs.
        /// </summary>
        public CommandFn Handlers;
    }

    public class Command : IDisposable
    {
        /// <summary>
        /// Anyone who wants to listen to the command when it occurs.
        /// </summary>
        public event CommandFn Handlers;

        /// <summary>
        /// The object we used to setup the command.
        /// </summary>
        internal CommandSetup Setup;

        /// <summary>
        /// Store the command manager plugin so we can tell it if we get destroyed.
        /// </summary>
        internal CommandManager ManagerPlugin;

        /// <summary>
        /// We don't want anyone else trying to create commands.
        /// </summary>
        internal Command(CommandManager manager)
        {
            this.ManagerPlugin = manager;
        }

        /// <summary>
        /// Only called by the manager when the command is invoked externally.
        /// </summary>
        internal void Invoke()
        {
            // If anyone is listening to the command...
            if (this.Handlers != null)
            {
                // Tell them that the command was invoked!
                this.Handlers();
            }
        }

        /// <summary>
        /// Dispose of the command.
        /// </summary>
        public void Dispose()
        {
            // As long as we weren't already disposed
            if (this.ManagerPlugin != null)
            {
                // Destroy our command
                this.ManagerPlugin.DestroyCommand(this);

                // Clear the manager plugin so we cannot destroy it again
                this.ManagerPlugin = null;

                // Clear our event handlers
                this.Handlers = null;

                // Clear out the setup object too to release any references
                this.Setup = null;
            }
        }
    }

    public class CommandManager : Plugin
    {
        /// <summary>
        /// The type of message we send or receieve.
        /// </summary>
        internal enum MessageType : byte
        {
            CreateCommand,
            DestroyCommand,
            CommandExecuted,
        }

        /// <summary>
        /// The name of the remote type we wish to instantiate.
        /// </summary>
        private const String RemoteType = "Skugo.StandardPlugins.CommandManager";

        /// <summary>
        /// Maintain a set of all commands by name (the names must be unique).
        /// </summary>
        private Dictionary<String, Command> Commands = new Dictionary<String, Command>();

        public CommandManager(Client client) :
            base(RemoteType, client)
        {
            // Just create the remote plugin (we should probably unify this!)
            this.CreateRemotePlugin();
        }

        // Validate that a keyword has no spaces in it, no consecutive capitols, and starts with a capitol letter
        private static void ValidateKeyword(String keyword, String type)
        {
            // Check that the keyword contains text
            if (keyword == null || keyword.Length == 0)
            {
                throw new Exception(type + " must contain text (cannot be null or empty)");
            }

            // Keyword must start uppercase
            if (Char.IsUpper(keyword[0]) == false)
            {
                throw new Exception(type + " must start with a capitol letter, in upper camel case form (NewProject, OpenLevel5, Aabb, Delete...)");
            }

            // Tells us if the previous character was uppercase
            Boolean wasUpper = false;

            // Loop through all the characters in the keyword
            foreach (var c in keyword)
            {
                // Keywords can only contain letters or digits
                if (Char.IsLetterOrDigit(c) == false)
                {
                    throw new Exception(type + " can only contain letters and numbers in upper camel case form (NewProject, OpenLevel5, Aabb, Delete...)");
                }

                // If the current character is upper-case
                if (Char.IsUpper(c))
                {
                    // Two characters cannot be in a row and uppercase
                    if (wasUpper == true)
                    {
                        throw new Exception(type + " must not have two consecutive capitol letters in upper camel case form (NewProject, OpenLevel5, Aabb, Delete...)");
                    }

                    // This one was uppercase
                    wasUpper = true;
                }
                else
                {
                    // We hit a non upper-case letter
                    wasUpper = false;
                }
            }
        }

        private void SendCreatedCommand(CommandSetup setup)
        {
            // Create a memory stream to write data to
            MemoryStream stream = new MemoryStream();

            // Create a binary writer to write the data
            BinaryWriter outgoing = new BinaryWriter(stream);

            // Write out a the frame buffer message
            outgoing.Write((Byte)MessageType.CreateCommand);

            // Write all command setup information
            outgoing.WriteAsciiString(setup.Name);
            outgoing.WriteAsciiString(setup.Description);
            outgoing.WriteAsciiString(setup.Category);
            outgoing.Write((Int32)setup.Icon);
            outgoing.Write((Int32)setup.ShortcutKey);
            outgoing.Write((Int32)setup.ShortcutModifiers);

            // Write out the number of keywords to the packet
            outgoing.Write(setup.Keywords.Count);

            // Validate all the keywords and send them
            foreach (var keyword in setup.Keywords)
            {
                // Validate the keyword
                ValidateKeyword(keyword, "Keyword");

                // Send the current keyword
                outgoing.WriteAsciiString(keyword);
            }

            // Send the packet
            this.Send(stream.ToArray());
        }

        public Command CreateCommand(CommandSetup setup)
        {
            // Validate the name and category
            ValidateKeyword(setup.Name, "Name");
            ValidateKeyword(setup.Category, "Category");

            // Check if our command set already has the name...
            if (this.Commands.ContainsKey(setup.Name))
            {
                throw new Exception("Cannot have two commands by the same name '" + setup.Name + "'");
            }

            // Send the remote client the created command
            this.SendCreatedCommand(setup);

            // Create a command object and set it up
            var command = new Command(this);
            command.Setup = setup;
            command.Handlers += setup.Handlers;
            setup.Handlers = null;

            // Add the command to our set
            this.Commands.Add(setup.Name, command);

            // Return the created command
            return command;
        }

        internal void DestroyCommand(Command command)
        {
            // Remove the command from our set
            this.Commands.Remove(command.Setup.Name);

            // Create a memory stream to write data to
            MemoryStream stream = new MemoryStream();

            // Create a binary writer to write the data
            BinaryWriter outgoing = new BinaryWriter(stream);

            // Write out a the frame buffer message
            outgoing.Write((Byte)MessageType.DestroyCommand);

            // Write all command setup information
            outgoing.WriteAsciiString(command.Setup.Name);

            // Send the packet
            this.Send(stream.ToArray());
        }

        public void CreateSubCategory(String parentCategory, String subCategory)
        {
            throw new NotImplementedException();
        }

        // Called when packet data is received from our external counterpart
        protected override void OnReceive(Byte[] data)
        {
            var incoming = data.ToBinaryReader();

            var messageType = (MessageType)incoming.ReadByte();

            switch (messageType)
            {
                case MessageType.CommandExecuted:
                {
                    // Which command was executed...
                    String commandName = incoming.ReadAsciiString();
                    
                    // Attempt to find the command
                    Command foundCommand;
                    if (this.Commands.TryGetValue(commandName, out foundCommand))
                    {
                        // Invoke the handlers on the command!
                        foundCommand.Invoke();
                    }
                    else
                    {
                        // Log the event for debugging
                        Framework.Log("The command '" + commandName + "' was not found (it could have been erased, but unlikely that the user would be able to click it that fast)");
                    }
                    break;
                }

                default:
                    throw new Exception("Unhandled message type!");
            }
        }
    }
}
